"""
Classes that define the file format for pecha files (projects).
"""
import os
import shutil
import tempfile
from zipfile import ZipFile

import sqlalchemy

from pechapub import error, util
from pechapub.datamodel import schema


EXT=".ppf"
DB_NAME = "data.sqlite"
TIBETAN_NAME = "tibetan.rtf"
TRANSLITER_NAME = "transliter.rtf"
TRANSLAT_NAME = "translat.rft"

TEXT_FILES = [
    TIBETAN_NAME,
    TRANSLITER_NAME,
    TRANSLAT_NAME,
]

BACKUP_NAME = "backup"
BACKUP_PROJECT_NAME = os.path.join("backup", "project")
BACKUP_FILES_NAME = os.path.join("backup", "files")
CONTENT_NAME = "content"
CONTENT_IMAGES_NAME = os.path.join("content", "images")
CONTENT_TEXT_NAME = os.path.join("content", "text")
FONTS_NAME = "fonts"

DIRECTORY_STRUCTURE = [
    BACKUP_NAME,
    BACKUP_PROJECT_NAME,
    BACKUP_FILES_NAME,
    CONTENT_NAME,
    CONTENT_IMAGES_NAME,
    CONTENT_TEXT_NAME,
    FONTS_NAME,
]

DEFAULT_PROJECT_BACKUP_COUNT = 5
DEFAULT_FILE_BACKUP_COUNT = 20


class ProjectAspect(object):

    def __init__(self, parent):
        self.parent = parent


class ProjectDatabase(ProjectAspect):

    def __init__(self, parent, create=False):
        super(ProjectDatabase, self).__init__(parent)
        self.path = os.path.join(
            self.parent.base_dir, self.parent.project_name, DB_NAME)
        if create:
            self._create_database()

    def _create_database(self):
        self.engine = sqlalchemy.create_engine("sqlite:///%s" % self.path)
        # XXX do this in a better way...
        metadata = sqlalchemy.MetaData()
        schema.create_schema(metadata)
        metadata.create_all(self.engine)
        # XXX bind the db session and set as a class attribute

    # XXX There will need to be convenience methods here for performing the
    # basic data read and write operations. This will make the GUI programming
    # much easier.

class ProjectTexts(ProjectAspect):

    def __init__(self, parent, create=False):
        super(ProjectTexts, self).__init__(parent)
        self.path = os.path.join(
            self.parent.base_dir, self.parent.project_name, CONTENT_TEXT_NAME)
        if create:
            self._create_text_files()

    def _create_text_files(self):
        for file_name in TEXT_FILES:
            fh = open(os.path.join(self.path, file_name), "w")
            fh.write("")
            fh.close()

    def _read(self, file_type):
        file_path = os.path.join(self.path, file_type)
        return open(file_path).read()

    def _write(self, file_type, text_data):
        file_path = os.path.join(self.path, file_type)
        open(file_path, "w").write(text_data)

    def read_tibetan(self):
        return self._read(TIBETAN_NAME)

    def write_tibetan(self, text):
        self._write(TIBETAN_NAME, text)

    def read_transliteration(self):
        return self._read(TRANSLITER_NAME)

    def write_transliteration(self, text):
        self._write(TRANSLITER_NAME, text)

    def read_translation(self):
        return self._read(TRANSLAT_NAME)

    def write_translation(self, text):
        self._write(TRANSLAT_NAME, text)


# XXX needs implementation and unit tests!
class ProjectImages(ProjectAspect):

    def read_image(self, image_name):
        pass

    def write_image(self, image_name, data):
        pass


class BaseProject(object):

    def __init__(self, project_path=""):
        self.temp_resources = util.TempResources()
        self.project_path = ""
        self.base_dir = ""
        self.project_name = ""
        self._parse_project_path(project_path)
        self._check_project_path(project_path)
        # placeholders for aspects
        self.db = None
        self.texts = None
        self.images = None

    def _parse_project_path(self, project_path=""):
        """
        This is put here instead of in the constructor because the identical
        functionality is needed elsewhere.
        """
        if not project_path:
            return
        self.base_dir = os.path.dirname(project_path)
        project_name = os.path.basename(project_path)
        self.project_name = self._get_project_name(project_name)
        if self.project_name != project_name:
            project_path = os.path.join(self.base_dir, self.project_name)
        self.project_path = project_path

    def _check_project_path(self, project_path):
        if os.path.exists(project_path):
            self.raiseError(
                error.ProjectCollisionError,
                "A project with that name already exists in the given "
                "directory. Please choose another name.")

    def _get_project_name(self, project_name):
        if not project_name.endswith(".ppf"):
            project_name += EXT
        return project_name

    def raiseError(self, exception, message):
        self.close()
        raise exception(message)

    def close(self):
        # remove temp files and directories
        self.temp_resources.clean_up()


class Project(BaseProject):
    """

    """
    def __init__(self, project_path=""):
        super(Project, self).__init__(project_path)
        if self.project_path:
            self._create_directories()
            self._set_aspects(create=True)

    def _create_directories(self):
        for dir_name in DIRECTORY_STRUCTURE:
            path = os.path.join(self.project_path, dir_name)
            os.makedirs(path)

    def _set_aspects(self, create=False):
        self.db = ProjectDatabase(self, create)
        self.texts = ProjectTexts(self, create)
        self.images = ProjectImages(self)

    def load(self, project_path):
        """
        When a project is loaded from a directory on the file system, the
        following needs to occur:
            * base_dir, project_name, and project_path need to get set
            * the db, texts, and images aspects need to be set
        """
        if not project_path.endswith(EXT):
            self.raiseError(
                error.ProjectLoadError,
                "PechaPub does not recognize projects that do not have the "
                "%s extension." % EXT)
        if not os.path.isdir(project_path):
            self.raiseError(
                error.ProjectLoadError,
                "To load an uncompressed PechaPub project, the project must "
                "be a directory and it must exist.")
        self._parse_project_path(project_path)
        self._set_aspects()

    def move(self, destination):
        if not os.path.exists(destination):
            self.raiseError(
                error.ProjectMoveError,
                "The project could not be moved to the given location. Make "
                "sure that the destination exists.")
        new_project_path = os.path.join(destination, self.project_name)
        self._check_project_path(new_project_path)
        shutil.move(self.project_path, new_project_path)
        self._parse_project_path(new_project_path)
        self._set_aspects()

    def backup(self):
        # XXX ideally, at this point all files in the project would be locked.
        temp_dir = self.temp_resources.get_dir()
        backup_dir = os.path.join(self.project_path, BACKUP_PROJECT_NAME)
        backup_name = "%s-%s" % (util.get_timestamp(), self.project_name)
        dest = os.path.join(backup_dir, backup_name)
        temp_dest = os.path.join(temp_dir, backup_name)
        shutil.copytree(self.project_path, temp_dest)
        shutil.move(temp_dest, dest)
        # XXX unlock all the files in the project
        return dest


class CompressedProject(BaseProject):
    """
    This class links a project directory with a zip file, and vice versa. If a
    compressed file is provided, a temporary directory is created where
    extracted contents may be accessed and written to. If a project object is
    provided, it is converted to a zip archive. If a project directory is
    provided, it's is converted to a project object and then to a zip archive.
    If the project directory doesn't exist, a new one is created.
    """
    # XXX hrm... it seems the only thing missing here is the final project
    # object... it should not be the source dir, but rather the temp dir
    # created from the extracted zip. Source dirs need to be converted to zips
    # and removed (backed up). Every instance's .project attribute should point
    # to the extracted zip temp directory.

    # XXX what about refactor #4? this init method has too much going on. We
    # should just be doing a couple things:
    #   1) making sure we can get the project name and base path
    #   2) up-calling
    # We don't want this class to work any hard than it has to when it's first
    # initialized. We should run all the other logic only when necessary.

    def __init__(self, project_object=None, project_path=""):
        if project_object and not isinstance(project_object, BaseProject):
            raise ValueError(
                "The project object must be a subclass of BaseProject.")
        if project_object:
            project_path = project_object.project_path
        elif not project_path:
            raise error.ProjectCreationError(
                "You must provide either a project object or the path to "
                "a project")
        super(CompressedProject, self).__init__()
        self.project = Project()
        # save the directory project's data into the compressed project's
        # attributes
        self._parse_project_path(project_path)
        self.extracted_dir = self.temp_resources.get_dir()
        self.extracted_project = os.path.join(
            self.extracted_dir, self.project_name)
        self.project.load(project_path)
        self.project.move(self.extracted_dir)
        self.zip = None

    def old___init__(self, project_object=None, project_path=""):
        if not project_object:
            project_object = self._get_project_object(project_path)
        super(CompressedProject, self).__init__(
            project_object.base_dir, project_object.project_name)
        self.zip = self.convert(project_object)
        self.project = project_object
        extract_base = self.temp_resources.get_dir()
        self.extract_path = os.path.join(
            extract_base,  project_object.project_name)

    def _get_project_object(self, project_path):
        """
        Give a project path, a project object is returned. This covers the
        following cases:
            * the provided path is a directory and it exists
            * the provided directory does not exist

        XXX This method does nothing if the project_path points to a zip file.
        (should it?)
        """
        base_dir = os.path.dirname(project_path)
        project_name = os.path.basename(project_path)
        if not os.path.isfile(project_path):
            project_object = Project(base_dir, project_name)
            # if there's no project, let's make one
            if not os.path.exists(project_path):
                project_object.create()
            # if there's a project there, then let's load it
            elif os.path.isdir(project_path):
                project_object.load(project_path)
        return project_object

    def prepare_temp_dir(self):
        pass

    def convert(self, project_object=None):
        """
        This method is intended to convert a file system projct to a compressed
        project.
        """
        if not project_object:
            project_object = self.project
        # backup current project
        project_object.backup()
        # compress moved project to the original project dir location, using
        # the original project's dir name as the compressed file name
        util.create_zip(
            zipped_project=self.project_path,
            source=self.extracted_project)
        print "\nproject_path:", self.project_path
        print "extracted_project:", self.extracted_project
        import pdb;pdb.set_trace()
        self.zip = ZipFile(self.project_path, "r")

    def backup(self, source):
        """
        Before edits, copies moves, etc., are performed on a project, we need
        to back it up. The source to be backed up can be a directory or a zip
        file.
        """
        # XXX a backup method needs to be created on the Project class, too

        # create a temporary directory where a backup can be saved

        # get the current datetime stamp, millisecond resolution

        # copy the zip file to the temporary directory

        # extract the zip file (???)
        # XXX this would be much easier with an async app... split this into
        # events. The backup needs to be performed in stages - one before the
        # opening/extracting, and then the second half once the extracting has
        # completed. Maybe we can split this method into two private methods...
        # _backup_stage1, _backup_stage2?
        # XXX and another question: where does the extracting take place?
        # always in on spot in the code? makes it easier to define when/where
        # backups occur... if not, we could be in for some more deep
        # thinking...

        # move the copied zip from from the temporary directory to the
        # ./backup/project directory

        # remove the temporary directory

    def save(self):
        """
        Bundle the contents of the working directory into a zip file and
        replace the old zip file with the new one.
        """
        #self.zip = ZipFile(path, "w")
        pass

    def _prepare_dir_project(self):
        # create a new, temporary directory
        temp_dir = self.temp_resources.get_dir()
        # move the directory to a temporary location
        shutil.move(self.project_path)
        return os.path.join(temp_dir, self.project_name)

    def extract(self):
        pass

    def close(self):
        super(CompressedProject, self).close()
        if hasattr(self, "zip") and self.zip:
            self.zip.close()
